package org.andengine.opengl.texture;

import android.content.res.AssetManager;

import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.bitmap.BitmapTextureFormat;
import org.andengine.opengl.util.GLState;
import org.andengine.util.adt.io.in.AssetInputStreamOpener;
import org.andengine.util.adt.io.in.IInputStreamOpener;
import org.andengine.util.debug.Debug;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

/**
 * (c) 2010 Nicolas Gramlich
 * (c) 2011 Zynga Inc.
 *
 * @author Nicolas Gramlich
 * @since 17:48:46 - 08.03.2010
 */
public class TextureManager {
    // ===========================================================
    // Constants
    // ===========================================================

    // ===========================================================
    // Fields
    // ===========================================================

    private final HashSet<ITexture> mTexturesManaged = new HashSet<ITexture>();
    private final HashMap<String, ITexture> mTexturesMapped = new HashMap<String, ITexture>();

    private final ArrayList<ITexture> mTexturesLoaded = new ArrayList<ITexture>();

    private final ArrayList<ITexture> mTexturesToBeLoaded = new ArrayList<ITexture>();
    private final ArrayList<ITexture> mTexturesToBeUnloaded = new ArrayList<ITexture>();

    private TextureWarmUpVertexBufferObject mTextureWarmUpVertexBufferObject;

    // ===========================================================
    // Constructors
    // ===========================================================

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    // ===========================================================
    // Methods
    // ===========================================================

    public synchronized void onCreate() {
        this.mTextureWarmUpVertexBufferObject = new TextureWarmUpVertexBufferObject();
    }

    public synchronized void onReload() {
        final HashSet<ITexture> managedTextures = this.mTexturesManaged;
        if (!managedTextures.isEmpty()) {
            for (final ITexture texture : managedTextures) { // TODO Can the use of the iterator be avoided somehow?
                texture.setNotLoadedToHardware();
            }
        }

        if (!this.mTexturesLoaded.isEmpty()) {
            this.mTexturesToBeLoaded.addAll(this.mTexturesLoaded); // TODO Check if addAll uses iterator internally!
            this.mTexturesLoaded.clear();
        }

        if (!this.mTexturesToBeUnloaded.isEmpty()) {
            this.mTexturesManaged.removeAll(this.mTexturesToBeUnloaded); // TODO Check if removeAll uses iterator internally!
            this.mTexturesToBeUnloaded.clear();
        }

        this.mTextureWarmUpVertexBufferObject.setNotLoadedToHardware();
    }

    public synchronized void onDestroy() {
        final HashSet<ITexture> managedTextures = this.mTexturesManaged;
        for (final ITexture texture : managedTextures) { // TODO Can the use of the iterator be avoided somehow?
            texture.setNotLoadedToHardware();
        }

        this.mTexturesToBeLoaded.clear();
        this.mTexturesLoaded.clear();
        this.mTexturesManaged.clear();
        this.mTexturesMapped.clear();

        this.mTextureWarmUpVertexBufferObject.dispose();
        this.mTextureWarmUpVertexBufferObject = null;
    }

    public synchronized boolean hasMappedTexture(final String pID) {
        if (pID == null) {
            throw new IllegalArgumentException("pID must not be null!");
        }
        return this.mTexturesMapped.containsKey(pID);
    }

    public synchronized ITexture getMappedTexture(final String pID) {
        if (pID == null) {
            throw new IllegalArgumentException("pID must not be null!");
        }
        return this.mTexturesMapped.get(pID);
    }

    public synchronized void addMappedTexture(final String pID, final ITexture pTexture) throws IllegalArgumentException {
        if (pID == null) {
            throw new IllegalArgumentException("pID must not be null!");
        } else if (pTexture == null) {
            throw new IllegalArgumentException("pTexture must not be null!");
        } else if (this.mTexturesMapped.containsKey(pID)) {
            throw new IllegalArgumentException("Collision for pID: '" + pID + "'.");
        }
        this.mTexturesMapped.put(pID, pTexture);
    }

    public synchronized ITexture removedMappedTexture(final String pID) {
        if (pID == null) {
            throw new IllegalArgumentException("pID must not be null!");
        }
        return this.mTexturesMapped.remove(pID);
    }

    /**
     * @param pTexture the {@link ITexture} to be loaded before the very next frame is drawn (Or prevent it from being unloaded then).
     * @return <code>true</code> when the {@link ITexture} was previously not managed by this {@link TextureManager}, <code>false</code> if it was already managed.
     */
    public synchronized boolean loadTexture(final ITexture pTexture) {
        if (pTexture == null) {
            throw new IllegalArgumentException("pTexture must not be null!");
        }

        if (this.mTexturesManaged.contains(pTexture)) {
            /* Just make sure it doesn't get deleted. */
            this.mTexturesToBeUnloaded.remove(pTexture);
            return false;
        } else {
            this.mTexturesManaged.add(pTexture);
            this.mTexturesToBeLoaded.add(pTexture);
            return true;
        }
    }

    /**
     * Must be called from the GL-{@link Thread}.
     *
     * @param pGLState
     * @param pTexture the {@link ITexture} to be loaded right now, if it is not loaded.
     * @return <code>true</code> when the {@link ITexture} was previously not managed by this {@link TextureManager}, <code>false</code> if it was already managed.
     */
    public synchronized boolean loadTexture(final GLState pGLState, final ITexture pTexture) throws IOException {
        if (pTexture == null) {
            throw new IllegalArgumentException("pTexture must not be null!");
        }

        if (!pTexture.isLoadedToHardware()) {
            pTexture.loadToHardware(pGLState);
        } else if (pTexture.isUpdateOnHardwareNeeded()) {
            pTexture.reloadToHardware(pGLState);
        }

        if (this.mTexturesManaged.contains(pTexture)) {
            /* Just make sure it doesn't get deleted. */
            this.mTexturesToBeUnloaded.remove(pTexture);
            return false;
        } else {
            this.mTexturesManaged.add(pTexture);
            this.mTexturesLoaded.add(pTexture);
            return true;
        }
    }

    /**
     * @param pTexture the {@link ITexture} to be unloaded before the very next frame is drawn (Or prevent it from being loaded then).
     * @return <code>true</code> when the {@link ITexture} was already managed by this {@link TextureManager}, <code>false</code> if it was not managed.
     */
    public synchronized boolean unloadTexture(final ITexture pTexture) {
        if (pTexture == null) {
            throw new IllegalArgumentException("pTexture must not be null!");
        }

        if (this.mTexturesManaged.contains(pTexture)) {
            /* If the Texture is loaded, unload it.
			 * If the Texture is about to be loaded, stop it from being loaded. */
            if (this.mTexturesLoaded.contains(pTexture)) {
                this.mTexturesToBeUnloaded.add(pTexture);
            } else if (this.mTexturesToBeLoaded.remove(pTexture)) {
                this.mTexturesManaged.remove(pTexture);
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Must be called from the GL-{@link Thread}.
     *
     * @param pGLState
     * @param pTexture the {@link ITexture} to be unloaded right now, if it is loaded.
     * @return <code>true</code> when the {@link ITexture} was already managed by this {@link TextureManager}, <code>false</code> if it was not managed.
     */
    public synchronized boolean unloadTexture(final GLState pGLState, final ITexture pTexture) {
        if (pTexture == null) {
            throw new IllegalArgumentException("pTexture must not be null!");
        } else if (pTexture.isLoadedToHardware()) {
            pTexture.unloadFromHardware(pGLState);
        }

        if (this.mTexturesManaged.contains(pTexture)) {
			/* Just make sure it doesn't get loaded. */
            this.mTexturesLoaded.remove(pTexture);
            this.mTexturesToBeLoaded.remove(pTexture);

            return true;
        } else {
            return false;
        }
    }

    public synchronized void updateTextures(final GLState pGLState) {
        final HashSet<ITexture> texturesManaged = this.mTexturesManaged;
        final ArrayList<ITexture> texturesLoaded = this.mTexturesLoaded;
        final ArrayList<ITexture> texturesToBeLoaded = this.mTexturesToBeLoaded;
        final ArrayList<ITexture> texturesToBeUnloaded = this.mTexturesToBeUnloaded;

		/* First reload Textures that need to be updated. */
        for (int i = texturesLoaded.size() - 1; i >= 0; i--) {
            final ITexture textureToBeReloaded = texturesLoaded.get(i);
            if (textureToBeReloaded.isUpdateOnHardwareNeeded()) {
                try {
                    textureToBeReloaded.reloadToHardware(pGLState);
                } catch (final IOException e) {
                    Debug.e(e);
                }
            }
        }

		/* Then load pending Textures. */
        final int texturesToBeLoadedCount = texturesToBeLoaded.size();

        if (texturesToBeLoadedCount > 0) {
            for (int i = texturesToBeLoadedCount - 1; i >= 0; i--) {
                final ITexture textureToBeLoaded = texturesToBeLoaded.remove(i);
                if (!textureToBeLoaded.isLoadedToHardware()) {
                    try {
                        textureToBeLoaded.loadToHardware(pGLState);

						/* Execute the warm-up to ensure the texture data is actually moved to the GPU. */
                        this.mTextureWarmUpVertexBufferObject.warmup(pGLState, textureToBeLoaded);
                    } catch (final IOException e) {
                        Debug.e(e);
                    }
                }
                texturesLoaded.add(textureToBeLoaded);
            }
        }

		/* Then unload pending Textures. */
        final int texturesToBeUnloadedCount = texturesToBeUnloaded.size();

        if (texturesToBeUnloadedCount > 0) {
            for (int i = texturesToBeUnloadedCount - 1; i >= 0; i--) {
                final ITexture textureToBeUnloaded = texturesToBeUnloaded.remove(i);
                if (textureToBeUnloaded.isLoadedToHardware()) {
                    textureToBeUnloaded.unloadFromHardware(pGLState);
                }
                texturesLoaded.remove(textureToBeUnloaded);
                texturesManaged.remove(textureToBeUnloaded);
            }
        }

		/* Finally invoke the GC if anything has changed. */
        if ((texturesToBeLoadedCount > 0) || (texturesToBeUnloadedCount > 0)) {
            System.gc();
        }
    }

    public synchronized ITexture getTexture(final String pID, final AssetManager pAssetManager, final String pAssetPath) throws IOException {
        return this.getTexture(pID, pAssetManager, pAssetPath, TextureOptions.DEFAULT);
    }

    public synchronized ITexture getTexture(final String pID, final AssetManager pAssetManager, final String pAssetPath, final TextureOptions pTextureOptions) throws IOException {
        if (this.hasMappedTexture(pID)) {
            return this.getMappedTexture(pID);
        } else {
            final ITexture texture = new BitmapTexture(this, new AssetInputStreamOpener(pAssetManager, pAssetPath), pTextureOptions);
            this.loadTexture(texture);
            this.addMappedTexture(pID, texture);

            return texture;
        }
    }

    public synchronized ITexture getTexture(final String pID, final IInputStreamOpener pInputStreamOpener) throws IOException {
        return this.getTexture(pID, pInputStreamOpener, TextureOptions.DEFAULT);
    }

    public synchronized ITexture getTexture(final String pID, final IInputStreamOpener pInputStreamOpener, final TextureOptions pTextureOptions) throws IOException {
        return this.getTexture(pID, pInputStreamOpener, BitmapTextureFormat.RGBA_8888, pTextureOptions);
    }

    public synchronized ITexture getTexture(final String pID, final IInputStreamOpener pInputStreamOpener, final BitmapTextureFormat pBitmapTextureFormat, final TextureOptions pTextureOptions) throws IOException {
        return this.getTexture(pID, pInputStreamOpener, pBitmapTextureFormat, pTextureOptions, true);
    }

    public synchronized ITexture getTexture(final String pID, final IInputStreamOpener pInputStreamOpener, final BitmapTextureFormat pBitmapTextureFormat, final TextureOptions pTextureOptions, final boolean pLoadToHardware) throws IOException {
        if (this.hasMappedTexture(pID)) {
            return this.getMappedTexture(pID);
        } else {
            final ITexture texture = new BitmapTexture(this, pInputStreamOpener, pBitmapTextureFormat, pTextureOptions);
            if (pLoadToHardware) {
                this.loadTexture(texture);
            }
            this.addMappedTexture(pID, texture);

            return texture;
        }
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}
